//This file contains the TokenManager class and related methods.
import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:copilot_proxy/common.dart';
import 'package:copilot_proxy/extension.dart';
import 'package:copilot_proxy/header_manager.dart';
import 'package:copilot_proxy/utils/utils.dart';

class Lock {
  int _useCount = 0;
  Completer<void> _lock = Completer<void>()..complete();

  void lock() {
    _useCount++;
    _lock = Completer<void>();
  }

  void unlock() {
    _useCount--;
    _lock.complete();
  }

  Future<void> wait() => _lock.future;

  bool get isBusy => _useCount > 0 || !_lock.isCompleted;
}

class TokenData {
  final String githubToken;
  String? copilotToken;
  DateTime? _expiry;
  int invalidatedCount = 0;
  int _useCount = 0;

  final _lock = Lock();
  final client = HttpClient();

  TokenData({
    required this.githubToken,
    HttpProxy? httpProxy,
    this.copilotToken,
  }) {
    if (httpProxy == null) return;
    setHttpProxy(httpProxy);
  }

  void setHttpProxy(HttpProxy httpProxy) {
    httpProxy.useCount++;
    final proxy = 'PROXY ${httpProxy.addr}';
    client.findProxy = (uri) => proxy;
  }

  bool get isInvalidated => invalidatedCount >= 3;

  bool isExpiry(DateTime now) {
    final time = _expiry;
    return time == null || now.isAfter(time);
  }

  ///设置过期
  void setExpiry() => _expiry = null;

  void lock() {
    _useCount++;
    _lock.lock();
  }

  void unlock() => _lock.unlock();

  Future<void> wait() => _lock.wait();

  bool get isBusy => _lock.isBusy;

  factory TokenData.fromJson(JsonMap tokenMap) {
    final tokenData = TokenData(
      githubToken: tokenMap['githubToken'],
      copilotToken: tokenMap['copilotToken'],
    );
    tokenData.invalidatedCount = tokenMap['invalidatedCount'] ?? 0;
    tokenData._useCount = tokenMap['useCount'] ?? 0;
    tokenData._expiry = DateTime.tryParse(tokenMap['expiry'] ?? '');
    return tokenData;
  }

  JsonMap toJson() {
    return {
      'githubToken': githubToken,
      'copilotToken': copilotToken,
      'expiry': _expiry?.toIso8601String(),
      'invalidatedCount': invalidatedCount,
      'useCount': _useCount,
    };
  }

  void updateByGithubResponse(JsonMap bodyJson) {
    final token = bodyJson['token'];
    final expiryAt = bodyJson['expires_at'];
    copilotToken = token;
    if (expiryAt == null) return;
    _expiry = DateTime.fromMillisecondsSinceEpoch(expiryAt * 1000);
    _useCount = 0;
  }
}

final copilotTokenUrl = 'https://api.github.com/copilot_internal/v2/token';

final copilotTokenUri = Uri.parse(copilotTokenUrl);

class HttpProxy {
  final String addr;
  int useCount = 0;

  HttpProxy(this.addr);
}

class TokenManager {
  TokenManager._();

  static final TokenManager instance = TokenManager._();

  factory TokenManager() => instance;

  final List<TokenData> _tokens = [];

  final List<HttpProxy> _proxies = [];

  Future<void> loadTokenData() async {
    await _loadHttpProxyByConfig();
    await _loadTokenDataByTokenFile();
    await _loadTokenDataByConfig();
  }

  Future<void> _loadHttpProxyByConfig() async {
    for (final httpProxyAddr in config.httpProxyAddrList) {
      _proxies.add(HttpProxy(httpProxyAddr));
    }
  }

  HttpProxy get _leastUsedProxy => _proxies.minBy((e) => e.useCount);

  Future<void> _loadTokenDataByTokenFile() async {
    final isProxy = _proxies.isNotEmpty;
    final tokenList = await loadJson('token.json');
    if (tokenList == null || tokenList is! JsonList) return;
    for (final tokenMap in tokenList) {
      final tokenData = TokenData.fromJson(tokenMap);
      _tokens.add(tokenData);
      if (!isProxy) continue;
      tokenData.setHttpProxy(_leastUsedProxy);
    }
  }

  Future<void> _loadTokenDataByConfig() async {
    final githubTokenList = List.of(config.githubTokenList);
    for (var e in _tokens) {
      githubTokenList.remove(e.githubToken);
    }
    final isProxy = _proxies.isNotEmpty;
    for (final githubToken in githubTokenList) {
      addGithubToken(githubToken, isProxy);
    }
  }

  void addGithubToken(String githubToken, [bool? isProxy]) {
    final tokenData = TokenData(githubToken: githubToken);
    if (isProxy ?? _proxies.isNotEmpty) tokenData.setHttpProxy(_leastUsedProxy);
    _tokens.add(tokenData);
  }

  //save token data
  Future<void> saveTokenData() => saveJson('token.json', _tokens);

  ///查找可用的token
  Future<TokenData?> _findCopilotTokenData() async {
    final now = DateTime.now();
    final busyTokens = <TokenData>[];
    //可用token列表
    final availableTokens = <TokenData>[];
    for (final tokenData in _tokens) {
      //token is invalid
      if (tokenData.isInvalidated) continue;
      //token is busy
      if (tokenData.isBusy) {
        busyTokens.add(tokenData);
        continue;
      }
      if (!tokenData.isExpiry(now) && tokenData.copilotToken != null) {
        availableTokens.add(tokenData);
        continue;
      }
      //token is expiry or copilotToken not exists
      tokenData.lock();
      final resp = await _requestCopilotToken(tokenData);
      if (resp.statusCode != HttpStatus.ok) {
        log('refreshCopilot: fail');
        tokenData.invalidatedCount++;
        tokenData.unlock();
        continue;
      }
      log('refreshCopilot: ok');
      final bodyJson = jsonDecode(await resp.getBody());
      tokenData.updateByGithubResponse(bodyJson);
      tokenData.unlock();
      availableTokens.add(tokenData);
    }
    if (availableTokens.isNotEmpty) {
      return availableTokens.minBy((e) => e._useCount);
    }
    if (busyTokens.isNotEmpty) {
      return busyTokens.minBy((e) => e._useCount);
    }
    return null;
  }

  Future<HttpClientResponse> _requestCopilotToken(TokenData tokenData) async {
    final request = await tokenData.client.getUrl(copilotTokenUri);
    final headers = HeaderManager.instance.getHeaders(copilotTokenUri.path);
    final customHeaders = {
      ...headers,
      HttpHeaders.authorizationHeader: ['token ${tokenData.githubToken}'],
    };
    request.setHeaders(customHeaders);
    return request.close();
  }

  Future<T?> useCopilotTokenData<T>(
    String? clientId,
    Future<T> Function(TokenData? tokenData) callback,
  ) async {
    final completer = Completer<T?>();
    //使用debouncer防止多次请求
    TokenDebouncer.run(clientId, () async {
      final tokenData = await _findCopilotTokenData();
      if (tokenData == null) {
        completer.complete(await callback(null));
        return;
      }
      while (tokenData.isBusy) {
        await tokenData.wait();
      }
      tokenData.lock();
      final result = await callback(tokenData);
      tokenData.unlock();
      completer.complete(result);
    });
    return completer.future;
  }
}

class TokenDebouncer {
  static final _clientIdDebouncerMap = <String, TokenDebouncer>{};
  static final duration = Duration(milliseconds: config.copilotDebounce);
  Timer? _timer;

  static void run(String? clientId, Future<void> Function() action) {
    final debouncer = _clientIdDebouncerMap[clientId ?? ''] ??= TokenDebouncer();
    debouncer._timer?.cancel();
    debouncer._timer = Timer(duration, () async {
      await action();
      _clientIdDebouncerMap.remove(clientId);
    });
  }
}
